/*
 * Copyright 1999-2017 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package me.itsoo.elasticbatis.parsing.druid.sql;

import lombok.extern.slf4j.Slf4j;
import me.itsoo.elasticbatis.parsing.druid.DbType;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.SQLExpr;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.SQLName;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.SQLObject;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.SQLReplaceable;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.SQLStatement;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.expr.SQLBinaryOpExpr;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.expr.SQLBinaryOperator;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.expr.SQLInListExpr;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.expr.SQLUnaryExpr;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.expr.SQLUnaryOperator;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.statement.SQLJoinTableSource;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.statement.SQLSelectQueryBlock;
import me.itsoo.elasticbatis.parsing.druid.sql.ast.statement.SQLTableSource;
import me.itsoo.elasticbatis.parsing.druid.sql.dialect.mysql.ast.MySqlObject;
import me.itsoo.elasticbatis.parsing.druid.sql.dialect.mysql.visitor.MySqlOutputVisitor;
import me.itsoo.elasticbatis.parsing.druid.sql.parser.Lexer;
import me.itsoo.elasticbatis.parsing.druid.sql.parser.ParserException;
import me.itsoo.elasticbatis.parsing.druid.sql.parser.SQLExprParser;
import me.itsoo.elasticbatis.parsing.druid.sql.parser.SQLParserFeature;
import me.itsoo.elasticbatis.parsing.druid.sql.parser.SQLParserUtils;
import me.itsoo.elasticbatis.parsing.druid.sql.parser.SQLStatementParser;
import me.itsoo.elasticbatis.parsing.druid.sql.parser.Token;
import me.itsoo.elasticbatis.parsing.druid.sql.visitor.SQLASTOutputVisitor;
import me.itsoo.elasticbatis.parsing.druid.sql.visitor.VisitorFeature;
import me.itsoo.elasticbatis.parsing.druid.util.FnvHash;
import me.itsoo.elasticbatis.parsing.druid.util.JdbcUtils;
import me.itsoo.elasticbatis.parsing.druid.util.MySqlUtils;
import me.itsoo.elasticbatis.parsing.druid.util.Utils;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

@Slf4j
public class SQLUtils {
    public final static Charset UTF8 = StandardCharsets.UTF_8;

    private final static SQLParserFeature[] FORMAT_DEFAULT_FEATURES = {
            SQLParserFeature.KeepComments, SQLParserFeature.EnableSQLBinaryOpExprGroup};

    public static FormatOption DEFAULT_FORMAT_OPTION = new FormatOption(true, true);
    public static FormatOption DEFAULT_LCASE_FORMAT_OPTION = new FormatOption(false, true);

    public static String toSQLString(SQLObject sqlObject, DbType dbType) {
        return toSQLString(sqlObject, dbType, null, null);
    }

    public static String toSQLString(SQLObject sqlObject, DbType dbType, FormatOption option) {
        return toSQLString(sqlObject, dbType, option, null);
    }

    public static String toSQLString(SQLObject sqlObject, DbType dbType, FormatOption option, VisitorFeature... features) {
        StringBuilder out = new StringBuilder();
        SQLASTOutputVisitor visitor = createOutputVisitor(out, dbType);
        if (option == null) {
            option = DEFAULT_FORMAT_OPTION;
        }
        visitor.setUppCase(option.isUppCase());
        visitor.setPrettyFormat(option.isPrettyFormat());
        visitor.setParameterized(option.isParameterized());
        int featuresValue = option.features;
        if (features != null) {
            for (VisitorFeature feature : features) {
                visitor.config(feature, true);
                featuresValue |= feature.mask;
            }
        }
        visitor.setFeatures(featuresValue);
        sqlObject.accept(visitor);
        return out.toString();
    }

    public static String toSQLString(SQLObject obj) {
        if (obj instanceof SQLStatement) {
            SQLStatement stmt = (SQLStatement) obj;
            return toSQLString(stmt, stmt.getDbType());
        }
        if (obj instanceof MySqlObject) {
            return toMySqlString(obj);
        }
        StringBuilder out = new StringBuilder();
        obj.accept(new SQLASTOutputVisitor(out));
        return out.toString();
    }

    public static String toMySqlString(SQLObject sqlObject) {
        return toMySqlString(sqlObject, (FormatOption) null);
    }

    public static String toMySqlString(SQLObject sqlObject, FormatOption option) {
        return toSQLString(sqlObject, DbType.mysql, option);
    }

    public static SQLExpr toSQLExpr(String sql, DbType dbType) {
        SQLExprParser parser = SQLParserUtils.createExprParser(sql, dbType);
        SQLExpr expr = parser.expr();
        if (parser.getLexer().token() != Token.EOF) {
            throw new ParserException("illegal sql expr : " + sql + ", " + parser.getLexer().info());
        }
        return expr;
    }

    public static SQLExpr toSQLExpr(String sql) {
        return toSQLExpr(sql, null);
    }

    public static SQLASTOutputVisitor createOutputVisitor(Appendable out, DbType dbType) {
        return createFormatOutputVisitor(out, null, dbType);
    }

    public static SQLASTOutputVisitor createFormatOutputVisitor(Appendable out, List<SQLStatement> statementList, DbType dbType) {
        if (dbType == null) {
            if (statementList != null && statementList.size() > 0) {
                dbType = statementList.get(0).getDbType();
            }

            if (dbType == null) {
                dbType = DbType.other;
            }
        }

        switch (dbType) {
            case mysql:
            case mariadb:
            case tidb:
                return new MySqlOutputVisitor(out);
            default:
                return new SQLASTOutputVisitor(out, dbType);
        }
    }

    public static List<SQLStatement> parseStatements(String sql, DbType dbType, SQLParserFeature... features) {
        SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, dbType, features);
        List<SQLStatement> stmtList = new ArrayList<SQLStatement>();
        parser.parseStatementList(stmtList, -1, null);
        if (parser.getLexer().token() != Token.EOF) {
            throw new ParserException("syntax error : " + parser.getLexer().info());
        }
        return stmtList;
    }

    public static List<SQLStatement> parseStatements(String sql, DbType dbType) {
        return parseStatements(sql, dbType, new SQLParserFeature[0]);
    }

    public static class FormatOption {
        private int features = VisitorFeature.of(VisitorFeature.OutputUCase, VisitorFeature.OutputPrettyFormat);

        public FormatOption(boolean ucase, boolean prettyFormat) {
            this(ucase, prettyFormat, false);
        }

        public FormatOption(boolean ucase, boolean prettyFormat, boolean parameterized) {
            this.features = VisitorFeature.config(this.features, VisitorFeature.OutputUCase, ucase);
            this.features = VisitorFeature.config(this.features, VisitorFeature.OutputPrettyFormat, prettyFormat);
            this.features = VisitorFeature.config(this.features, VisitorFeature.OutputParameterized, parameterized);
        }

        public boolean isUppCase() {
            return isEnabled(VisitorFeature.OutputUCase);
        }

        public boolean isPrettyFormat() {
            return isEnabled(VisitorFeature.OutputPrettyFormat);
        }

        public boolean isParameterized() {
            return isEnabled(VisitorFeature.OutputParameterized);
        }

        public void config(VisitorFeature feature, boolean state) {
            features = VisitorFeature.config(features, feature, state);
        }

        public final boolean isEnabled(VisitorFeature feature) {
            return VisitorFeature.isEnabled(this.features, feature);
        }
    }

    public static long hash(String sql, DbType dbType) {
        Lexer lexer = SQLParserUtils.createLexer(sql, dbType);
        StringBuilder buf = new StringBuilder(sql.length());
        for (; ; ) {
            lexer.nextToken();
            Token token = lexer.token();
            if (token == Token.EOF) {
                break;
            }
            if (token == Token.ERROR) {
                return Utils.fnv_64(sql);
            }
        }
        return buf.hashCode();
    }

    public static SQLExpr not(SQLExpr expr) {
        if (expr instanceof SQLBinaryOpExpr) {
            SQLBinaryOpExpr binaryOpExpr = (SQLBinaryOpExpr) expr;
            SQLBinaryOperator op = binaryOpExpr.getOperator();
            SQLBinaryOperator notOp = null;
            switch (op) {
                case Equality:
                    notOp = SQLBinaryOperator.LessThanOrGreater;
                    break;
                case LessThanOrEqualOrGreaterThan:
                    notOp = SQLBinaryOperator.Equality;
                    break;
                case LessThan:
                    notOp = SQLBinaryOperator.GreaterThanOrEqual;
                    break;
                case LessThanOrEqual:
                    notOp = SQLBinaryOperator.GreaterThan;
                    break;
                case GreaterThan:
                    notOp = SQLBinaryOperator.LessThanOrEqual;
                    break;
                case GreaterThanOrEqual:
                    notOp = SQLBinaryOperator.LessThan;
                    break;
                case Is:
                    notOp = SQLBinaryOperator.IsNot;
                    break;
                case IsNot:
                    notOp = SQLBinaryOperator.Is;
                    break;
                default:
                    break;
            }
            if (notOp != null) {
                return new SQLBinaryOpExpr(binaryOpExpr.getLeft(), notOp, binaryOpExpr.getRight());
            }
        }
        if (expr instanceof SQLInListExpr) {
            SQLInListExpr inListExpr = (SQLInListExpr) expr;
            SQLInListExpr newInListExpr = new SQLInListExpr(inListExpr);
            newInListExpr.getTargetList().addAll(inListExpr.getTargetList());
            newInListExpr.setNot(!inListExpr.isNot());
            return newInListExpr;
        }
        return new SQLUnaryExpr(SQLUnaryOperator.Not, expr);
    }

    public static String normalize(String name) {
        return normalize(name, null);
    }

    public static String normalize(String name, DbType dbType) {
        return _normalize(name, dbType, false);
    }

    private static String _normalize(String name, DbType dbType, boolean isForced) {
        return _normalize(name, dbType, isForced, true);
    }

    private static String _normalize(String name, DbType dbType, boolean isForced, boolean isTrimmed) {
        if (name == null) {
            return null;
        }
        if (name.length() > 2) {
            char c0 = name.charAt(0);
            char x0 = name.charAt(name.length() - 1);
            if ((c0 == '"' && x0 == '"') || (c0 == '`' && x0 == '`') || (c0 == '\'' && x0 == '\'')) {
                String normalizeName = name.substring(1, name.length() - 1);
                if (isTrimmed) {
                    normalizeName = normalizeName.trim();
                }
                int dotIndex = normalizeName.indexOf('.');
                if (dotIndex > 0) {
                    if (c0 == '`') {
                        normalizeName = normalizeName.replaceAll("`\\.`", ".");
                    }
                }
                if (!isForced) {
                    if (JdbcUtils.isMysqlDbType(dbType)) {
                        if (MySqlUtils.isKeyword(normalizeName)) {
                            return name;
                        }
                    }
                }
                return normalizeName;
            }
        }
        return name;
    }

    public static String forcedNormalize(String name, DbType dbType) {
        return _normalize(name, dbType, true);
    }

    public static boolean nameEquals(SQLName a, SQLName b) {
        if (a == b) {
            return true;
        }
        if (a == null || b == null) {
            return false;
        }
        return a.nameHashCode64() == b.nameHashCode64();
    }

    public static boolean nameEquals(String a, String b) {
        if (a == b) {
            return true;
        }
        if (a == null || b == null) {
            return false;
        }
        if (a.equalsIgnoreCase(b)) {
            return true;
        }
        String normalize_a = normalize(a);
        String normalize_b = normalize(b);
        return normalize_a.equalsIgnoreCase(normalize_b);
    }

    public static boolean replaceInParent(SQLExpr expr, SQLExpr target) {
        if (expr == null) {
            return false;
        }
        SQLObject parent = expr.getParent();
        if (parent instanceof SQLReplaceable) {
            return ((SQLReplaceable) parent).replace(expr, target);
        }
        return false;
    }

    public static boolean replaceInParent(SQLTableSource cmp, SQLTableSource dest) {
        if (cmp == null) {
            return false;
        }
        SQLObject parent = cmp.getParent();
        if (parent instanceof SQLSelectQueryBlock) {
            SQLSelectQueryBlock queryBlock = (SQLSelectQueryBlock) parent;
            if (queryBlock.getFrom() == cmp) {
                queryBlock.setFrom(dest);
                return true;
            }
        }
        if (parent instanceof SQLJoinTableSource) {
            SQLJoinTableSource join = (SQLJoinTableSource) parent;
            return join.replace(cmp, dest);
        }
        return false;
    }

    public static String desensitizeTable(String tableName) {
        if (tableName == null) {
            return null;
        }

        tableName = normalize(tableName);
        long hash = FnvHash.hashCode64(tableName);
        return Utils.hex_t(hash);
    }
}
